package com.chenl.provider.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 分布式锁
 * /
 */
@Slf4j
@Component
public class RedisLock {
    @Resource(name = "MyRedisTemplate")
    private RedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString().replace("-", "") + "-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    private static final DefaultRedisScript<Long> WATCH_DOG_SCRIPT;
    ThreadLocal<String> threadLocal4Value = new ThreadLocal();
    ThreadLocal<Thread> threadLocal4Thread = new ThreadLocal();

    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>(
                "if(redis.call)('get',KEYS[1]) == ARGV[1] then\n"
                        + "return redis.call('del',KEYS[1])\n"
                        + "end\n"
                        + "return 0", Long.class);
        WATCH_DOG_SCRIPT = new DefaultRedisScript<>(
                "if redis.call('get', KEYS[1]) == ARGV[1] then \n" +
                        "return redis.call('expire', KEYS[1],ARGV[2])\n" +
                        "else return '0' end", Long.class);
    }

    //获取锁(手动释放+看门狗)
    public Boolean lock(String name, Long time, TimeUnit timeUnit) {
        BigDecimal bigDecimal = parseTimeToSecond(time, timeUnit);
        Integer timeLong = Integer.valueOf(bigDecimal.toString());
        //获取线程标识
        String value = UUID.randomUUID().toString().replace("-", "") + "-" + Thread.currentThread().getId();
        threadLocal4Value.set(value);
        //获取锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, value, time, timeUnit);
        if (flag) {
            Thread thread = watchDog(timeLong, name, value);
            threadLocal4Thread.set(thread);
        }
        return Boolean.TRUE.equals(flag);
    }

    //获取锁(手动释放)
    public Boolean simpleLock(String name, Long time, TimeUnit timeUnit) {
        //获取线程标识
        String threadId = UUID.randomUUID().toString().replace("-", "") + "-" + Thread.currentThread().getId();
        threadLocal4Value.set(threadId);
        //获取锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, time, timeUnit);
        return Boolean.TRUE.equals(flag);
    }

    //获取锁(自动释放)
    public Boolean lockockAutoRelease(String name, Long time, TimeUnit timeUnit) {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, time, timeUnit);
        return Boolean.TRUE.equals(flag);
    }

    public void unlock(String name) {
        Thread thread = threadLocal4Thread.get();
        if (thread != null) {
            thread.interrupt();//停止守护线程
        }
        //调用lua脚本，删除key 解锁
        stringRedisTemplate.execute(UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                threadLocal4Value.get());
        threadLocal4Value.remove();
        threadLocal4Thread.remove();
    }

    public void simpleunlock(String name) {
        //调用lua脚本
        stringRedisTemplate.execute(UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                threadLocal4Value.get());
        threadLocal4Value.remove();
    }

    //重试获取锁(手动释放),reTryCount为重试次数,reTryinterval为重试间隔毫秒数
    public Boolean tryLock(String name, Long time, TimeUnit timeUnit, Integer reTryCount, Long reTryinterval) throws InterruptedException {
        //获取线程标识
        String threadId = UUID.randomUUID().toString().replace("-", "") + "-" + Thread.currentThread().getId();
        //获取锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, time, timeUnit);
        int count = 0;
        while (!flag && count < reTryCount) {
            Thread.sleep(reTryinterval);
            flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, time, timeUnit);
            count++;
        }
        if (flag) {
            threadLocal4Value.set(threadId);
        }
        return Boolean.TRUE.equals(flag);
    }

    /**
     * 创建续命子线程
     *
     * @param time 操作预期耗时
     * @param name key
     * @return 续命线程 Thread
     */
    public Thread watchDog(int time, String name, String value) {
        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(time * 2 / 3);
                    //重置时间
                    log.info("watchDog---重置时间：key:{},value:{},time:{}", KEY_PREFIX + name,value,time);
                    Long result = (Long) stringRedisTemplate.execute(WATCH_DOG_SCRIPT, Collections.singletonList(KEY_PREFIX + name), value, time);
                    //stringRedisTemplate.expire(KEY_PREFIX + name,time,TimeUnit.SECONDS);
                    if (!"1".equals(String.valueOf(result))) {
                        return;
                    }
                } catch (Exception e) {
                    log.info("watchDog---异常：{}", e.getMessage());
                    return;
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        return thread;
    }

    public static BigDecimal parseTimeToSecond(Long time, TimeUnit timeUnit) {
        if (TimeUnit.MILLISECONDS.equals(timeUnit)) {
            return BigDecimal.valueOf(time).divide(BigDecimal.valueOf(1000));
        } else if (TimeUnit.SECONDS.equals(timeUnit)) {
            return BigDecimal.valueOf(time);
        } else if (TimeUnit.MINUTES.equals(timeUnit)) {
            return BigDecimal.valueOf(time).multiply(BigDecimal.valueOf(60));
        } else if (TimeUnit.HOURS.equals(timeUnit)) {
            return BigDecimal.valueOf(time).multiply(BigDecimal.valueOf(60 * 60));
        } else if (TimeUnit.DAYS.equals(timeUnit)) {
            return BigDecimal.valueOf(time).multiply(BigDecimal.valueOf(60 * 60 * 24));
        } else {
            return BigDecimal.valueOf(time);
        }
    }
}